package world;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import world.conditions.Condition;
import world.edges.Border;
import world.edges.Wall;
import world.placement.Direction;
import world.placement.Position;
import world.square.Rock;
import world.square.Square;
import world.square.Subject;
import world.square.Teleportation;
import world.square.Temperature;
import world.square.TemperatureScale;
import world.square.Transparent;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;
import exceptions.IllegalSquarePlacementException;

/**
 * A class representing a dungeon.
 * @param 	<S>
 * 			The type parameter, which is a square.
 * 
 * @invar	The maximum size in the x-direction is valid
 * 			| isValidMaximumSize (getMaxXCoord())
 * 
 * @invar	The maximum size in the y-direction is valid
 * 			| isValidMaximumSize (getMaxYCoord())
 * 
 * @invar	The maximum size in the z-direction is valid
 * 			| isValidMaximumSize (getMaxZCoord())
 * 
 * @invar	The squares and positions of the squares are valid.
 * 			| for each square in squares.keySet():
 * 			|	canHaveAsSquare(square)
 * 			| for each position in squares.values():
 * 			| 	canHaveAsPosition(position)
 * @author Frederic Mes, Wim Marynissen
 * @version 1.0
 */

public abstract class Dungeon<S extends Square> implements Observer { 	
	
	/**
	 * Create a standard dungeon, with maximum coordinate value 20 in each direction.
	 * 
	 * @effect  A new dungeon is initialized with standard maximum coordinates
     *        | this(20,20,20) 
	 */
	public Dungeon (){	
		this (20, 20, 20);
	}

    /**
     * Creates a new dungeon with specified maximum dimensions.
     * 
     * @param 	maxXCoord
     * 			The maximum size this dungeon has in the x-direction
     * @param 	maxYCoord
     * 			The maximum size this dungeon has in the y-direction
     * @param 	maxZCoord
     * 			The maximum size this dungeon has in the z-direction
     * @post 	The maximum coordinate in the x-direction is equal to maxXCoord.
     *          | new.getMaxXCoord() == maxXCoord
     * @post 	The maximum coordinate in the y-direction is equal to maxYCoord.
     *          | new.getMaxYCoord() == maxYCoord
     * @post 	The maximum coordinate in the z-direction is equal to maxZCoord.
     *          | new.getMaxZCoord() == maxZCoord
     * @throws	IllegalArgumentException
	 * 			The specified value is unsuitable as maximum x coordinate.
	 * 			| !canHaveAsNewMaxCoord(maxXCoord,getMaxXCoord())
	 * @throws	IllegalArgumentException
	 * 			The specified value is unsuitable as maximum y coordinate.
	 * 			| !canHaveAsNewMaxCoord(maxYCoord,getMaxYCoord())
	 * @throws	IllegalArgumentException
	 * 			The specified value is unsuitable as maximum z coordinate.
	 * 			| !canHaveAsNewMaxCoord(maxZCoord,getMaxZCoord())
     */
	public Dungeon(long xMaxCoord, long yMaxCoord, long zMaxCoord) throws IllegalArgumentException{
		this.maxXCoord=xMaxCoord;
		this.maxYCoord=yMaxCoord;
		this.maxZCoord=zMaxCoord;
	}
	
	/**
	 * Returns the size this dungeon has in the x-direction
	 */
	@Basic
	public long getMaxXCoord() {
		return maxXCoord;
	}
	/**
	 * @param 	maxXCoord
	 * 			The maximum size this dungeon has in the x-direction
	 * @post	The new maximum coordinate in the x-direction is equal to the specified value.
	 * 			| new.getMaxXCoord() == maxXCoord
	 * @throws	IllegalArgumentException
	 * 			The specified value is unsuitable as maximum x coordinate.
	 * 			| !canHaveAsNewMaxCoord(maxXCoord,getMaxXCoord())
	 */
	@Raw
	public void setMaxXCoord(long maxXCoord) throws IllegalArgumentException{
		if (isValidNewMaxCoord(maxXCoord, getMaxXCoord()))
			this.maxXCoord = maxXCoord;
		else
			throw new IllegalArgumentException("Invalid maximum x coordinate.");
	}
	
	/**
	 * Returns the size this dungeon has in the y-direction
	 */
	@Basic
	public long getMaxYCoord() {
		return maxYCoord;
	}
	/**
	 * @param 	maxYCoord
	 * 			The maximum size this dungeon has in the y-direction
	 * @post	The new maximum coordinate in the y-direction is equal to the specified value.
	 * 			| new.getMaxYCoord() == maxYCoord
	 * @throws	IllegalArgumentException
	 * 			The specified value is unsuitable as maximum y coordinate.
	 * 			| !canHaveAsNewMaxCoord(maxYCoord,getMaxYCoord())
	 */
	@Raw
	public void setMaxYCoord(long maxYCoord) throws IllegalArgumentException {
		if (isValidNewMaxCoord(maxYCoord, getMaxYCoord()))
			this.maxYCoord = maxYCoord;
		else
			throw new IllegalArgumentException("Invalid maximum y coordinate.");
	}
	

	/**
	 * Returns the size this dungeon has in the z-direction
	 */
	@Basic
	public long getMaxZCoord() {
		return maxZCoord;
	}
	/**
	 * @param 	maxZCoord
	 * 			The maximum size this dungeon has in the z-direction
	 * @post	The new maximum coordinate in the z-direction is equal to the specified value.
	 * 			| new.getMaxZCoord() == maxZCoord
	 * @throws	IllegalArgumentException
	 * 			The specified value is unsuitable as maximum z coordinate.
	 * 			| !canHaveAsNewMaxCoord(maxZCoord,getMaxZCoord())
	 */
	@Raw
	public void setMaxZCoord(long maxZCoord) throws IllegalArgumentException{
		if (isValidNewMaxCoord(maxZCoord, getMaxZCoord()))
			this.maxZCoord = maxZCoord;
		else
			throw new IllegalArgumentException("Invalid maximum z coordinate.");
	}
		
	/**
	 *  Method to test if a value can be used as the new maximum coordinate in a specific direction.
	 * @param 	newMaxCoord
	 * 		  	The new maximum coordinate.
	 * @param 	maxCoord
	 * 		  	The current maximum coordinate.
	 * @return 	True if the new maximum coordinate is bigger than the current maximum and bigger than 0.
	 * 		  	| result == (isValidMaximumSize(newMaxCoord)) && (newMaxCoord > maxCoord)
	 */
	@Raw
	public static boolean isValidNewMaxCoord(long newMaxCoord, long maxCoord){
		return (isValidMaximumSize(newMaxCoord)) && (newMaxCoord > maxCoord);
	}
	
	/**
	 * Returns whether or not the given maximum is a valid maximum size.
	 * @param 	maxCoord
	 * 		  	The size to be checked.
	 * @return 	result == (maxCoord >= 0)
	 */
	@Raw
	public static boolean isValidMaximumSize(long maxCoord){
		return maxCoord >= 0;
	}
	
	/**
	 * Variable registering the maximum coordinate in the x-direction.
	 */
	private long maxXCoord;
	/**
	 * Variable registering the maximum coordinate in the y-direction.
	 */
	private long maxYCoord;
	/**
	 * Variable registering the maximum coordinate in the z-direction.
	 */
	private long maxZCoord;
	
	/**
	 * Method to add a square to the dungeon.
	 * 
	 * @param 	square
	 * 		  	The square that has to be added to the dungeon.
	 * @param 	position
	 * 		  	The position in the dungeon this square has to be added to.
	 * @throws 	IllegalSquarePlacementException
	 * 		  	The provided square can't be placed at the given position.
	 * 		  	| !canHaveSquareAtPosition(square,position)
	 * @post 	The square is added at the specified position
	 * 			| new.getSquares().get(square).equals(position)
	 * @post	If the containing dungeon is a composite, this square has also needs to be set in the composite square.
	 * 			| if (getContainingDungeon() instanceof Composite)
	 *			| 	then new.getContainingDungeon().getCompositeSquares().contains(this)
	 * @effect	Borders get updated.
	 * 			| updateBorders(position)
	 * @effect 	Spaces get balanced.
	 * 			| balanceSpaces()
	 */
	@Raw
	public void addSquare (S square, Position position) throws IllegalSquarePlacementException{	
		if (!canHaveSquareAtPosition(square,position))
			throw new IllegalSquarePlacementException();
		
		squares.put(square, position);
		updateBorders(position);
		
		if (getContainingDungeon() instanceof Composite<?>)
			((Composite<S>) getContainingDungeon()).setCompositeSquares(this);
		
		balanceSpaces();
	}

	
	/**
	 * Removes a square from the dungeon.
	 * @param 	square
	 * 		  	The square to be removed from the dungeon.
	 * @post 	The square no longer is part of this dungeon.
	 * 			| !(new this).containsSquare(square)
	 */
	public void removeSquare (S square){
		if (this.containsSquare(square))
			getSquares().remove(square);
	}

	/**
	 * Removes a square from the dungeon on the given position.
	 * @param 	position
	 * 		  	The position on which the square has to be removed.
	 * @post 	If there is a square at the specified position, it will be removed.
	 * 			| if(this.containsPosition(position))
	 *		 	|	then getSquares().remove(getSquareFromPosition(position))
	 */
	public void removeSquareWithPosition(Position position){
		if(this.containsPosition(position))
			getSquares().remove(getSquareFromPosition(position));
	}
	
	/**
	 * Gets the neighbours of the square on the given position in all directions,
	 *  and makes sure the mutual borders of the squares are consistent.
	 * 
	 * @param 	position
	 * 		  	The borders of the neighbours of the square on this position are updated.
	 * 
	 * @effect 	The borders are shared in each direction
	 * 			| for each direction in getNeighbours(position1).keySet():
     *          |	shareBorders (position1, returnOriginalPosition(position1.getPositionInDirection(direction)), direction)
     * @throws 	IllegalArgumentException
     * 			The position is non-effective.
     * 			| position == null
	 */
	public void updateBorders(Position position) throws IllegalArgumentException{
		if(position == null)
			throw new IllegalArgumentException("Non-effective position");
		HashMap<Direction, S> neighbours = this.getNeighbours(position);
		for (Direction direction : neighbours.keySet()){
			shareBorders(position, returnOriginalPosition(position.getPositionInDirection(direction)), direction);
		}
	}
	
	
	/**
	 * If the squares on the given positions do not already have mutual BorderElements,
	 * their borders will be set so that they share the same BorderElement.
	 * In setting new borders, the closeness of the BorderElement in the border decides how the new border
	 * will look like. The border which has the BorderElement with the highest closeness will be shared.
	 * 
	 * @param 	position1
	 * 		 	The position of the first square.
	 * @param 	position2
	 * 		  	The Position of the second square.
	 * @param 	direction
	 * 		  	The direction of the border of the first square.
	 * @throws 	IllegalArgumentException
	 * 		   	The provided positions or direction are non-effective.
	 * 		   	| (position1 == null) || (position2 == null)  || (direction == null)
	 * @post   	Both squares will have the same border element on their common border.
	 * 			| getSquareFromPosition (position1).getBorder(direction).getBorderElement().equals(getSquareFromPosition (position2).getBorder(direction.getOpposite()).getBorderElement())
	 * 			|	|| getSquareFromPosition (position1).getBorder(direction.getOpposite).getBorderElement().equals(getSquareFromPosition (position2).getBorder(direction).getBorderElement())
	 *
	 */
	public void shareBorders (Position position1, Position position2, Direction direction) throws IllegalArgumentException{
		if ((position1 == null) || (position2 == null ) || direction == null)
			throw new IllegalArgumentException();
		
		Border border1;
		Border border2;
		if (direction.equals(Direction.WEST) || direction.equals(Direction.EAST)){
			border1 =  getSquareFromPosition (position1).getBorder(direction);
			border2 = getSquareFromPosition (position2).getBorder(direction.getOpposite());
		}else{
			border1 =  getSquareFromPosition (position1).getBorder(direction.getOpposite());
			border2 = getSquareFromPosition (position2).getBorder(direction);	
		}

			
		Border bordernew;
		if (border1 != null && border2 != null){
			if(border1.isTerminated() || border2.isTerminated()){
				removeTerminated(border1,border2,position1,position2);}
			else{
				if (border1.getBorderElement().getImportanceInSharing() >= border2.getBorderElement().getImportanceInSharing()){
					bordernew = border1.oppositeBorder();
					getSquareFromPosition(position2).addBorders(bordernew);
					bordernew.setSquare(getSquareFromPosition(position2));
				}
				else{
					bordernew = border2.oppositeBorder();
					getSquareFromPosition (position1).addBorders (bordernew);
					bordernew.setSquare(getSquareFromPosition (position1));
					}
			}
		}else if (border1 != null){
			if(border1.isTerminated()){
				removeTerminated(border1,border2,position1,position2);}
			else{
				bordernew = border1.oppositeBorder();
				getSquareFromPosition (position2).addBorders (bordernew);
				bordernew.setSquare(getSquareFromPosition (position2));
			}
		}else if (border2 != null){
			if(border2.isTerminated()){
				removeTerminated(border1,border2,position1,position2);}
			else{
				bordernew = border2.oppositeBorder();
				getSquareFromPosition (position1).addBorders (bordernew);
				bordernew.setSquare(getSquareFromPosition (position1));
			}
		}
	}
	
	/**
	 * Remove terminated borders from squares. Private method.
	 * @note	No seperate documentation included, this method is part of shareBorders().
	 */
	private void removeTerminated(Border border1,Border border2,Position position1,Position position2){
		if((border1 != null) && border1.isTerminated()){
			try{
				getSquareFromPosition (position1).removeBorder(border1);
				if(border2 != null)
					getSquareFromPosition (position2).removeBorder(border2);
			}
			catch(IllegalArgumentException e){
				getSquareFromPosition (position1).addBorders(border1);
				getSquareFromPosition (position2).addBorders(border2);
			}
		}
		if((border2 != null) && border2.isTerminated()){
			try{
				if(border2 != null)
					getSquareFromPosition (position1).removeBorder(border1);
				getSquareFromPosition (position2).removeBorder(border2);
			}
			catch(IllegalArgumentException e){
				getSquareFromPosition (position1).addBorders(border1);
				getSquareFromPosition (position2).addBorders(border2);
			}
		}
			
	}
	
	/**
	 * Returns a hashmap containing the squares of the dungeons and their respective positions.
	 */
	@Basic @Raw
	public Map<S,Position> getSquares() {
		return squares;
	}
	
	/**
	 * A map containing the squares in the dungeon and their respective positions.
	 */
	protected Map<S, Position> squares = new HashMap <S, Position>();
	
	
	/**
     * Returns whether or not the square is inside this dungeon
     * 
     * @param 	square
     *          The square that will be checked
     * @return 	Whether or not this dungeon contains the square
     * 			result == getSquares().containsKey(square)
     * @throws 	IllegalArgumentException
     *          The square has is non-effective or terminated.
     *          |(square == null) || (square.isTerminated())
     */
	@Raw
    public boolean containsSquare(S square) throws IllegalArgumentException{
            if(!isValidSquare(square))
                    throw new IllegalArgumentException("Square not initialized");
            return getSquares().containsKey(square);
    }
    
	/**
	 * Returns whether or not this dungeon contains (a square on) the given position.
	 * 
	 * @param 	position
	 * 		  	The position that will be checked.
	 * @return 	True if this dungeon contains a position that has the same coordinates as the given position.
	 * 			| for each squarePosition in getSquares().values():
	 * 		    |	if (positionsEqual (squarePosition, position))
	 * 		    |		then result == true
	 * @throws	IllegalArgumentException
	 * 			The provided position is non-effective
	 *			| position == null
	 */
	@Raw
	public boolean containsPosition(Position position) throws IllegalArgumentException{
		if(position == null)
			throw new IllegalArgumentException("Non-effective position");
		for (Position squarePosition : getSquares().values()) {
			if (squarePosition.equals(position)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Returns the exact object saved in the hashmap squares with the same coordinates as the given position if available.
	 * 
	 * @param 	position
	 * 		  	The position of which has to be checked whether or not a square is in the dungeon with the same coordinates.
	 * @return 	The position in the dungeon with the same coordinates as the given position.
	 * 			| for each pos in getSquares().values():
	 * 		    |	if (positionsEqual (pos, position))
	 * 		    |		then result == pos
	 * @throws	IllegalArgumentException
	 * 			The provided position is non-effective
	 *			| position == null  
	 */
	public Position returnOriginalPosition(Position position) throws IllegalArgumentException{
		if (containsPosition (position)){
			for (Position pos : getSquares().values()){
				if (pos.equals(position)){
					return  pos;
				}	
			}
		}
		return null;
	}
	
	/**
	 * Returns the square of the given position in this dungeon.
	 * @param 	position
	 * 		  	The given position
	 * @return 	The square in this dungeon on the given position.
	 * 		   	| for each entry in getSquares().entrySet():
	 * 		   	|	if (entry.equals (position))
	 * 		   	|		then result == entry.getKey()
	 * @throws	IllegalArgumentException
	 * 			The provided position does not contain any square.
	 *			| !containsPosition(position)
	 * @throws	IllegalArgumentException
	 * 			The provided position is non-effective
	 *			| position == null  
	 */
	public S getSquareFromPosition(Position position) throws IllegalArgumentException{
		if (!containsPosition(position))
			throw new IllegalArgumentException("Position not taken.");		
	
		Iterator<Entry<S, Position>> iter = getSquares().entrySet().iterator();
		S square = null;
		while (iter.hasNext()) {
		    Entry<S, Position> entry = iter.next();
		    if (entry.getValue().equals(position)) {
		        square = entry.getKey();
		    }
		}
		return square;
	}
	
	

	/**
	 * Returns whether or not the dungeon can contain the given square.
	 * 
	 * @param 	square
	 * 		  	The square to be checked.
	 * @return  The square must be effective.
	 * 			| if (square == null)
	 * 		    |  then result == false
	 * @return  | for each squareInDungeon in getSquares():
	 * 			| 	if (square.equals(squareInDungeon))
	 * 			|		then result == false
	 * @return  | if (square.isSlippery())
	 * 			|   if(((getNbOfSlipperySquares()+1) / (getNbOfSquares()+1))>0.20)
	 *			|		then result == false
	 *		    
	 */
	public boolean canHaveAsSquare(S square){
		if (!isValidSquare(square))
			return false;
		for (S squareInDungeon : getSquares().keySet()){
			if(square.equals(squareInDungeon))
				return false;
		}
		
		if (square.isSlippery())
			if(((getNbOfSlipperySquares()+1) / (getNbOfSquares()+1))>0.20)
				return false;
		return true;
	}	
	
	/**
	 * Check is a square is valid.
	 * @param 	square
	 * 		  	The square to validate.
	 * @return	The square must be effective and can't be terminated.
	 * 			| result == (square != null) && !square.isTerminated()
	 */
	public boolean isValidSquare(S square){
		return (square != null) && !square.isTerminated();
	}
	
	/**
	 * Returns whether or not the dungeon can contain the given position.
	 * 
	 * @param 	position
	 * 		 	The position to be validated.
	 * @return	The position must be effective.
	 * 		    | if (position == null)
	 * 		    | 	then result == false
	 * @return  The position cannot already contain a square.
	 * 			|for each positionInDungeon in getSquares()
	 * 			| 	if (position.equals(positionInDungeon))
	 * 			|		then result == false
	 * @return  A square can't be place at a position of equal x, y and z coordinates.
	 * 			|if (position.getXCoord() == position.getYCoord() && position.getXCoord() == position.getZCoord())
	 *			|	then result == false   
	 * @return	The coordinates must be smaller than the maximum coordinate values.
	 * 			| (xCoord <= getMaxXCoord()) && (yCoord <= getMaxYCoord())
	 * 			| 		&& (zCoord <= getMaxZCoord())
	 */
	public boolean canHaveAsPosition(Position position) {
		if (position == null)
			return false;
		long xCoord = position.getXCoord();
		long yCoord = position.getYCoord();
		long zCoord = position.getZCoord();
		for (Position positionInDungeon : getSquares().values()) {
			if (position.equals(positionInDungeon))
				return false;
		}
		if (xCoord == yCoord && xCoord == zCoord)
			return false;
		return ((xCoord <= getMaxXCoord()) && (yCoord <= getMaxYCoord())
					&& (zCoord <= getMaxZCoord()));
	}
	
	/**
	 * @param 	square
	 * 			The square to validate.
	 * @param 	position
	 * 		 	The position to be validated.
	 * @return	The square and the position must be proper.
	 * 			| if(!canHaveAsSquare || ! canHaveAsPosition)
	 * 			|	then result == false
	 * @return	A transparent square can't be neighbours with a rock or a 
	 * 			square with a wall on the border.
	 * 			| if(square instanceof Transparent)
	 * 			|	for each direction in getNeighbours(getPositionFromSquare(square)).keySet()
	 * 			|		if(neighbours.get(direction).getBorder(direction.getOpposite()).getBorderElement() instanceof Wall)
	 * 			|			then result == false
	 * 			| if(square instanceof Rock)
	 * 			|	for each neighbour in getNeighbours(getPositionFromSquare(square)).values()
	 * 			|		if(neighbour instanceof Transparent)
	 * 			|			then result == false
	 * 
	 */
	public boolean canHaveSquareAtPosition(S square,Position position){
		if(!canHaveAsSquare((S) square) || !canHaveAsPosition(position))
			return false;
		if(square instanceof Transparent){
			HashMap<Direction, S> neighbours = getNeighbours(position);
			for(Direction direction : neighbours.keySet()){
				if(neighbours.get(direction).getBorder(direction.getOpposite()).getBorderElement() instanceof Wall)
					return false;
			}
		}
		if(square instanceof Rock){
			for(S neighbour : getNeighbours(position).values()){
				if(neighbour instanceof Transparent)
					return false;
			}
		}
		return true;
	}
	
	/**
	 * Returns whether or not the given squares are neighbours.
	 * @param 	square1
	 * 		  	The first square.
	 * @param 	square2
	 * 		  	The second square.
	 * @return 	True if the positions of the squares are next to each other.
	 * 			| if (areNeighbouringPositions (getPositionFromSquare(square1), getPositionFromSquare(square2)))
	 *		    |  	then result ==  true
	 */
	public boolean areNeighbours(S square1, S square2){
		if (areNeighbouringPositions (getSquares().get(square1), getSquares().get(square2)))
			return true;
		return false;
	}
	
	/**
	 * Returns whether or not the given positions are neighbouring.
	 * @param 	position1
	 * 		  	The first position.
	 * @param 	position2
	 * 		  	The second position.
	 * @return 	True if the square on the second position is a neighbour of the square on the first one.
	 * 			| if (getNeighbours(position1).containsValue(getSquareFromPosition(returnOriginalPosition(position2)))){
	 *			|	then result == true
	 */
	public boolean areNeighbouringPositions (Position position1, Position position2){
		if (getNeighbours(position1).containsValue(getSquareFromPosition(returnOriginalPosition(position2)))){
			return true;
		}
		return false;
	}
	
	/**
	 * Returns the neighbours of the square on the given position.
	 * @param 	position
	 * 		  	The position of which the neighbours have to be checked.
	 * @return 	| for each direction in Direction.values():
	 * 		   	| 	if (containsPosition(position.getPositionInDirection(direction)))
	 * 		   	| 		then result.containsValue (getSquareFromPosition (position))
	 * @throws 	IllegalArgumentException
	 * 			The provided position is non-effective.
	 * 			| position == null
	 */
	public HashMap<Direction, S> getNeighbours(Position position) throws IllegalArgumentException {
		if(position == null)
			throw new IllegalArgumentException("Non-effective position");
		HashMap<Direction, S> neighbours = new HashMap<Direction, S>();
		
		for (Direction direction : Direction.values()) {
			if (containsPosition(position.getPositionInDirection(direction))) {
				neighbours.put(direction,
						getSquareFromPosition(returnOriginalPosition(position
								.getPositionInDirection(direction))));
			}
		}
		return neighbours;
	}
	
	/**
	 * Returns the number of slippery squares in the dungeon.
	 * @return 	Count all the slippery squares and return them.
	 * 			| for each square in getSquares().keySet():
	 * 		   	|	 if (square.isSlippery)
	 * 		   	|		then result += 1.
	 */
	public int getNbOfSlipperySquares() {
		int nbOfSlipperySquares = 0;
		for (S square : getSquares().keySet())
			if (square.isSlippery())
				nbOfSlipperySquares++;
		return nbOfSlipperySquares;
	}
	
	/**
	 * Returns the number of squares in the dungeon.
	 * @return square.size()
	 */
	public int getNbOfSquares() {
		return squares.size();
	}

	/**
	 * Returns a list containing all lists of the spaces.
	 * @return  All the squares are added to the list, in their proper space.
	 * 			| for each square in getSquares().keySet():
	 * 		   	| 	if (!checkedList.contains(square)) 
	 * 	 	   	|		then
	 * 		   	|			let
	 *		   	|				singleSpace = buildSpace(square,new ArrayList<Square>())
	 *		   	|			in	
	 *		   	|				result.contains(singleSpace)
	 */
	public ArrayList<ArrayList<S>> getSpaces() {
		ArrayList<ArrayList<S>> spaces = new ArrayList<ArrayList<S>>();
		checkedList.clear();

		for (S square : getSquares().keySet()) {
			if (!checkedList.contains(square)) {
				ArrayList<S> singleSpace = buildSpace(square,
						new ArrayList<S>());
				spaces.add(singleSpace);
			}
		}
		return spaces;
	}
	
	/**
	 * Returns the space containing the given square.
	 * @param 	square
	 * 		  	The square around which the space has to be made.
	 * @param 	singleSpace
	 * 		  	A list containing squares in the space
	 * @return 	The square is added tot the space.
	 * 			|result.contains (square)
	 * @effect 	| for each neighbour in getOpenNeighbours (square):
	 * 		   	|	if (!checkedList.contains(neighbour)) 
	 *		   	|		then buildSpace(neighbour, singleSpace)
	 */
	public ArrayList<S> buildSpace(S square, ArrayList<S> singleSpace) {
		
		singleSpace.add(square); //O(1)
		checkedList.add(square); //O(1)

		ArrayList<S> openNeighbours = getOpenNeighbours(square); //
		for (S neighbour : openNeighbours) { 
			if (!checkedList.contains(neighbour)) {
				buildSpace(neighbour, singleSpace);
			}
		} //O(n) n = # squares in space
		return singleSpace;
	}
	/**
	 * 
	 * Returns the artificial space containing the given square.
	 * A artificial space is a space including all squares accessible trough teleportation.
	 * @param 	square
	 * 		  	The square around which the virtual space has to be made.
	 * @param 	singleSpace
	 * 		  	A list containing squares in the artificial space
	 * @return 	The square is added tot the virtual space.
	 * 			|result.contains (square)
	 * @effect 	| for each neighbour in getOpenNeighbours(square):
	 * 		   	|	if (!checkedList.contains(neighbour)) 
	 *		   	|		then buildSpace(neighbour, singleSpace)
	 * @effect 	| for each neighbour in square.getDestinations():
	 * 		   	|	if (!checkedList.contains(neighbour)) 
	 *		   	|		then buildSpace(neighbour, singleSpace)
	 */
	@SuppressWarnings("unchecked")
	public ArrayList<S> buildArtificialSpace(S square, ArrayList<S> singleSpace) {
		singleSpace.add(square); //O(1)
		checkedList.add(square); //O(1)

		ArrayList<S> neighbours = getOpenNeighbours(square);
		if(square instanceof Teleportation)
			for(Square destination : ((Teleportation) square).getDestinations())
				neighbours.add((S) destination);
		if(neighbours != null){
			for (S neighbour : neighbours) { 
				if (!checkedList.contains(neighbour)) {
					buildArtificialSpace(neighbour, singleSpace);
				}
			} //O(n) n = # squares in space
		}
		return singleSpace;
	}
	
	/**
	 * A list used in the calculations of the spaces.
	 */
	private ArrayList<S> checkedList = new ArrayList<S>();
	
	/**
	 * Returns the open neighbours of a square. A neighbour is open to a square when there is no
	 * wall or closed door in between.
	 * @param 	square
	 * 		  	The square we want the open neighbours from.
	 * @return  The accessible neighbours for the provided square.
	 * 			| for each direction in getNeighbours(getPositionFromSquare(square)).keySet():
	 * 		   	|	let
	 * 		   	|		if (direction.equals(Direction.NORTH) || direction.equals(Direction.SOUTH))
	 * 		   	|			then border = square.getBorder(direction.getOpposite())
	 *		   	|		else
	 *		   	|			then border = square.getBorder(direction);
	 * 		   	|	in
	 * 		   	|		if (border == null){
     *		   	|			then result.contains(neighbour);
	 *		   	|		else if(!border.getBorderElement().isObstacle()){
	 *		   	|			then result.contains(neighbour);
	 * @return	If the square has no valid position, return null.
	 * 			| if(getPositionFromSquare == null)
	 * 			|	 then (result == null)
	 */
	public ArrayList<S> getOpenNeighbours(S square) {
		HashMap<Direction, S> neighbours = new HashMap<Direction,S>();
		try{
			neighbours = getNeighbours(getSquares().get(square));
		}
		catch(IllegalArgumentException e){
			return null;
		}
		ArrayList<S> openNeighbours = new ArrayList<S>();
		Border border;
		for (Direction direction : neighbours.keySet()){
			S neighbour = neighbours.get(direction);

			if (direction.equals(Direction.NORTH) || direction.equals(Direction.SOUTH))
				border = square.getBorder(direction.getOpposite());
			else
				border = square.getBorder(direction);
			if (border == null){
				openNeighbours.add(neighbour);
			}else if(!border.getBorderElement().isObstacle()){
				openNeighbours.add(neighbour);
			}
			
		} //O(m) m = number of directions with neighbours (between 0 and 6)
		return openNeighbours;
	}
	
	/**
	 * Returns whether or not the given square has open neighbours.
	 * @param 	square
	 * 		  	The square we want the check.
	 * @return 	result == getOpenNeighbours(square).size() != 0
	 */
	public boolean hasOpenNeighbours(S square){
        return getOpenNeighbours(square).size() != 0;
	}
	
	/**
	 * Returns the size of the space containing the given square.
	 * @param 	square
	 * 		  	The square which has to be in the space we check the size from.
	 * @return 	|for each space in getSpaces():
	 * 		   	|	if (space.contains(square))
	 * 		   	|		then result == space.size()	
	 * @throws	IllegalArgumentException
	 * 			The provided square is invalid.
	 *			| !isValidSquare(square) 	
	 */
	public int sizeOfSpaceWithSquare (S square) throws IllegalArgumentException{
		if(!isValidSquare(square))
			throw new IllegalArgumentException("Invalid square");
		ArrayList<ArrayList<S>> spaces = getSpaces();
		for (ArrayList<S> space : spaces){
			if (space.contains(square))
				return space.size();
		}
		return 0;
	}
	
	
	/**
	 * Balances the temperatures of the squares in the given space.
	 * @param 	space
	 * 		  	The space in which all temperatures have to be balanced.
	 * @post   	The temperatures in each space are balanced.
	 * 			| (new this).areTemperaturesBalanced() == true
	 */
	public void balanceTemperature(ArrayList<S> space) {
		if (space.size() == 1) {   
			S square = space.get(0);
				if (square instanceof Rock) {
					square.setTemperature(averageNeighbourTemperature(square));
				}
		}
		else{
			BigDecimal totalTemp = new BigDecimal("0");
			BigDecimal avgTemp;
			for (S square : space) {
				totalTemp = totalTemp.add(square.getTemperature().getValue(
						TemperatureScale.CELCIUS));
			}
			avgTemp = totalTemp.divide(new BigDecimal(space.size()), 2,
					RoundingMode.HALF_EVEN);
			for (S square : space){
				square.setTemperature(new Temperature(avgTemp,
						TemperatureScale.CELCIUS));
			}
		}
	}
	
	
	
	/**
	 * Balance all of the spaces.
	 * @effect	Balance the temperatures of all squares in a space.
	 * 			| for each space in getSpaces()
	 * 			| 	balanceTemperature(space)
	 * @effect	Balance the humidities of all squares  in a space.
	 * 			| for each space in getSpaces()
	 * 			|	balanceHumidity(space)
	 */
	public void balanceSpaces() {
		for (ArrayList<S> space : getSpaces()) {
			balanceTemperature(space);
			balanceHumidity(space);
		}
	}
	
	/**
	 * Update all of the borders of the squares in this dungeon.
	 * @effect	Each border gets updated.
	 * 			| for each position in getSquares().values():
	 * 			|	updateBorders(position)
	 */
	public void updateAllBorders(){
		for(Position position : getSquares().values())
			updateBorders(position);
	}
	
	/**
	 * Get the average temperature of the surrounding neighbours of a square.
	 * @param 	square
	 * 			The square for which to take the average of neighbour temperatures.
	 * @return	An average of the temperatures of the neighbours of this square.
	 * 			| for each square in getNeighbours(getPositionFromSquare(square)).values()
	 * 			| 	totalTemp = totalTemp.add(neighbour.getTemperature().getValue(
	 *			|	TemperatureScale.CELCIUS)
	 *			| if (neighbours.size() != 0)
	 *		 	|	then averageTemp = totalTemp.divide(new BigDecimal(neighbours.size()))
	 *			| result == new Temperature(averageTemp,TemperatureScale.CELCIUS)
	 * @throws	IllegalArgumentException
	 * 			The provided square is invalid.
	 *			| !this.containsSquare(square) 
	 */
	public Temperature averageNeighbourTemperature(S square) throws IllegalArgumentException{
		if(!this.containsSquare(square))
			throw new IllegalArgumentException("The provided square is not part of this dungeon.");
			
		BigDecimal totalTemp;
		totalTemp = new BigDecimal("0");
		HashMap<Direction, S> neighbours = getNeighbours(getSquares().get(square));
		for (Square neighbour : neighbours.values()) {
			totalTemp = totalTemp.add(neighbour.getTemperature().getValue(
					TemperatureScale.CELCIUS));
		}
		BigDecimal averageTemp = BigDecimal.ZERO;
		if (neighbours.size() != 0)
			 averageTemp = totalTemp.divide(new BigDecimal(neighbours.size())); 
		return new Temperature(averageTemp,TemperatureScale.CELCIUS);
	}
	
	/**
	 * Returns whether or not all temperatures in all spaces are balanced.
	 * @return	Each square in the same space must have the same temperature.
	 * 			| for each space in spaces:
	 * 			|	for each square in space:
	 * 			|		if (!square.getTemperature().getValue(TemperatureScale.CELCIUS).equals(squares.getTemperature().getValue(TemperatureScale.CELCIUS))
	 * 			|			then result == false
	 * @return	A rock's temperature must be the average of the temperature of the neighbours.
	 * 			| for each space in spaces:
	 * 			|	if ((space.size() == 1) && (space.get(0) instanceof Rock)
	 * 			|			&& !square.getTemperature().getValue(TemperatureScale.CELCIUS).equals(averageNeighbourTemperature(square)))
	 * 			|				then result == false
	 * 
	 */
	public boolean areTemperaturesBalanced(){
		ArrayList<ArrayList<S>> spaces = getSpaces();
		for (ArrayList<S> space : spaces){
			if (space.size() == 1) {   
				S square = space.get(0);
					if (square instanceof Rock) {
						if(!square.getTemperature().equals(averageNeighbourTemperature(square)))
								return false;
					}
			}
			Square square = space.get(0); 
			for (S otherSquare : space){ 
				if (!square.getTemperature().getValue(TemperatureScale.CELCIUS).equals(otherSquare.getTemperature().getValue(TemperatureScale.CELCIUS)) ){
					return false;
				}
			}
		}	
		return true;
	}
	
	/**
	 * Balances the humidities of the squares in the given space.
	 * @param 	space
	 * 		  	The space in which all humidities have to be balanced.
	 * @post 	The humidities in each space are balanced.
	 * 			| areHumiditiesBalanced() == true
	 */
	public void balanceHumidity(ArrayList<S> space){
		BigDecimal totalHum = new BigDecimal(0);
		BigDecimal avgHum;
		for (S square : space){
			totalHum = totalHum.add(square.getHumidity());
		}
		avgHum = totalHum.divide(new BigDecimal(space.size()),2, RoundingMode.HALF_EVEN);
		for(S square : space){
				square.setHumidity(avgHum);
		}
	}	
	

	/**
	 * Returns whether or not all humidities in all spaces are balanced.
	 * @return	Each square in a space must have the same humidity.
	 * 			| for each space in spaces:
	 * 			|	for each square in space:
	 * 			|		if (!square.getHumidity().equals(squares.getHumidity()))
	 * 			|			then result == false
	 */
	public boolean areHumiditiesBalanced(){
		ArrayList<ArrayList<S>> spaces = getSpaces();
		for (ArrayList<S> space : spaces){
			Square squareOne = space.get(0);
			for (S square : space){
				if (!squareOne.getHumidity().equals(square.getHumidity()) ){
					return false;
				}
			}
		}	
		return true;
	}
	
	/**
	 * Returns whether or not the given squares are in the same space.
	 * 
	 * @param 	firstSquare
	 * 		  	The first square
	 * @param 	secondSquare
	 * 		  	The second square
	 * @return	True if the space of firstSquare contains secondSquare.
	 * 			| result == buildSpace(firstSquare,new ArrayList<Square>())
	 * 			|				.contains(secondSquare)
	 * @note	Complexity: O(n) with n the number of squares in the space. 
	 */
	public boolean areInSameSpace(S firstSquare, S secondSquare) {
		checkedList.clear(); 
		ArrayList<S> space = buildSpace(firstSquare,
				new ArrayList<S>()); 
		return space.contains(secondSquare); 
	}
	

	/**
	 * Returns whether or not one can navigate from a certain square to another given square.
	 * 
	 * @param 	fromSquare
	 * 		  	The square to move from.
	 * @param 	toSquare
	 * 		  	The square to check if can be moved to.
	 * @return	The square to move from, must be valid.
	 * 			| if(!isValidSquare(fromSquare))
	 * 			| 	then result == false
	 * @return	The square to move to, must be valid.
	 * 			| if(!isValidSquare(toSquare))
	 * 			| 	then result == false
	 * @return	True if the space of fromSquare contains toSquare,
	 * 			or if the space of the destination of a teleporting square contains toSquare.
	 * 			| result == buildVirtualSpace(fromSquare,new ArrayList<Square>())
	 * 			|				.contains(toSquare)
	 */
	public boolean canNavigateTo(S fromSquare,S toSquare){ 
		if(!isValidSquare(fromSquare)||!isValidSquare(toSquare))
			return false;
		checkedList.clear();
		ArrayList<S> artificialSpace = buildArtificialSpace(fromSquare,new ArrayList<S>());
		return artificialSpace.contains(toSquare);
	}
	
	/**
	 * Returns the dungeon which contains this dungeon
	 */
	public abstract Composite<S> getContainingDungeon();
	/**
	 * Sets the the containing dungeon to the given value.
	 * @param	dungeon
	 * 			The dungeon to set as containing dungeon.
	 * @post	The new containing dungeon is set to the provided one.
	 * 			| (new this).getContainingDungeon == dungeon
	 */
	public abstract void setContainingDungeon(Composite<S> dungeon);
	/**
	 * Returns the rootdungeon of this dungeon.
	 * 
	 * @return 	The rootdungeon is this if it has no containing dungeon.
	 * 		   	|if (getContainingDungeon() == null)
	 * 		   	|	then result == this
	 * @return 	The rootdungeon is the rootdungeon of this containing dungeon of this.
	 * 		   	| result == getContainingDungeon().getRootDungeon()
	 */
	public abstract Dungeon<S> getRootDungeon();
	
	/**
	 * A method returning all the possible positions that can exist in this dungeon.
	 * 
	 * @return	Each of the returned positions is a valid position in this dungeon.
	 * 			| for each postion in result:
	 * 			|	canHaveAsPosition(position) == true
	 */
	public abstract ArrayList<Position> getAllPossiblePositions();
	
	
	/**
	 * Returns the fixed position.
	 */
	@Basic
	public Position getFixedPosition(){
		return fixedPosition;
	}
	
	/**
	 * Sets the fixed position
	 * @param 	position
	 * 		  	The new fixed position.
	 * @post	The new position is equal to the provided value.
	 * 			| (new this).getFixedPosition) = position
	 * @throws 	IllegalArgumentException
	 * 			The provided position is non-effective.
	 * 			| position == null
	 */
	public void setFixedPosition(Position position) throws IllegalArgumentException{
		if(position == null)
			throw new IllegalArgumentException("Non-effective position");
		fixedPosition = position;
	}
	
	/**
	 * Variable containing the fixed (relative) position of the dungeon in comparison to the containing dungeon.
	 */
	public Position fixedPosition;
	
	/**
     * Return an iterator returning all the squares in this dungeon.
     *
     * @return  If the iterator of the squares in a specific dungeon has a next element,
     * 			 the resulting iterator has a next element, and vice versa.
     *        	| result.hasNext() == currentIterator.hasNext()
     */
	public abstract Iterator<S> iterator();
        
	
	/**
	 * Returns a list of all squares satisfying a certain condition
	 * @param 	condition
	 * 		  	The condition the squares need to satisfy
	 * @return 	The condition applies for all the returned results.
	 * 			|for each square in result:
	 * 		   	|	condition.check(square) == true
	 */
    public List<S> getSquaresWithCondition(Condition condition){
    	List<S> list = new ArrayList<S>();
    	Iterator<S> iterator = this.iterator();
    	while (iterator.hasNext()){
    		S square = iterator.next();
    		if (condition.check(square))
    			list.add(square);
    	}
    	return list;
    }
	
	
	/**
     * Terminate this dungeon.
     * 
     * @post 	This dungeon is terminated
     *       	| new.isTerminated() == true
     */
    public void terminate(){
            for (S square : squares.keySet())
                    square.terminate();
            squares.clear();
            isTerminated = true;
    }
    
    /**
     * Check whether this square is terminated.
     */
    @Basic @Raw
    public boolean isTerminated() {
		return this.isTerminated;
	}
    
    /**
     * Variable registering whether this square is terminated.
     */
    public boolean isTerminated = false;

    /**
     * Update the state of the squares, after a change.
     * Method used in the ObjectObserver pattern.
     * @effect	Update the borders
     * 			| updateAllBorders()
     * @effect 	Balance the spaces.
     * 			| balanceSpaces()
     */
	public void update(Subject o) {
		updateAllBorders();
		balanceSpaces();
	}
}
